// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:async';

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/category_axis.dart';
import 'package:charts_grasp/src/chart/axis/datetime_category_axis.dart';
import 'package:charts_grasp/src/chart/base/chart_base.dart';
import 'package:charts_grasp/src/chart/chart_behavior/chart_behavior.dart';
import 'package:charts_grasp/src/chart/chart_series/series.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/rendering_details.dart';
import 'package:charts_grasp/src/common/user_interaction/tooltip_rendering_details.dart';
import 'package:charts_grasp/src/common/utils/enum.dart';
import 'package:charts_grasp/src/common/utils/typedef.dart';
import 'package:charts_grasp/src/core/tooltip_internal.dart';
import 'package:flutter/material.dart';

export 'package:charts_grasp/src/core/core.dart' show DataMarkerType, TooltipAlignment;

class TooltipBehavior {
  TooltipBehavior(
      {this.textStyle,
      ActivationMode? activationMode,
      int? animationDuration,
      bool? enable,
      double? opacity,
      Color? borderColor,
      double? borderWidth,
      double? duration,
      bool? shouldAlwaysShow,
      double? elevation,
      bool? canShowMarker,
      ChartAlignment? textAlignment,
      int? decimalPlaces,
      TooltipPosition? tooltipPosition,
      bool? shared,
      this.color,
      this.header,
      this.format,
      this.builder,
      this.shadowColor,})
      : animationDuration = animationDuration ?? 350,
        textAlignment = textAlignment ?? ChartAlignment.center,
        activationMode = activationMode ?? ActivationMode.singleTap,
        borderColor = borderColor ?? Colors.transparent,
        borderWidth = borderWidth ?? 0,
        duration = duration ?? 3000,
        enable = enable ?? false,
        opacity = opacity ?? 1,
        shouldAlwaysShow = shouldAlwaysShow ?? false,
        canShowMarker = canShowMarker ?? true,
        tooltipPosition = tooltipPosition ?? TooltipPosition.auto,
        elevation = elevation ?? 2.5,
        decimalPlaces = decimalPlaces ?? 3,
        shared = shared ?? false;

  final bool enable;

  final Color? color;

  final String? header;

  final double opacity;

  final TextStyle? textStyle;

  final int decimalPlaces;

  final String? format;

  final int animationDuration;

  final bool canShowMarker;

  final ActivationMode activationMode;

  final Color borderColor;

  final double borderWidth;

  final ChartWidgetBuilder<dynamic>? builder;

  final Color? shadowColor;

  final double elevation;

  final bool shouldAlwaysShow;

  final double duration;

  final ChartAlignment textAlignment;

  final TooltipPosition tooltipPosition;

  final bool shared;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is TooltipBehavior &&
        other.textStyle == textStyle &&
        other.activationMode == activationMode &&
        other.animationDuration == animationDuration &&
        other.enable == enable &&
        other.opacity == opacity &&
        other.borderColor == borderColor &&
        other.borderWidth == borderWidth &&
        other.duration == duration &&
        other.shouldAlwaysShow == shouldAlwaysShow &&
        other.elevation == elevation &&
        other.canShowMarker == canShowMarker &&
        other.textAlignment == textAlignment &&
        other.decimalPlaces == decimalPlaces &&
        other.tooltipPosition == tooltipPosition &&
        other.shared == shared &&
        other.color == color &&
        other.header == header &&
        other.format == format &&
        other.builder == builder &&
        other.shadowColor == shadowColor;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[
      textStyle,
      activationMode,
      animationDuration,
      enable,
      opacity,
      borderColor,
      borderWidth,
      duration,
      shouldAlwaysShow,
      elevation,
      canShowMarker,
      textAlignment,
      decimalPlaces,
      tooltipPosition,
      shared,
      color,
      header,
      format,
      builder,
      shadowColor,
    ];
    return Object.hashAll(values);
  }

  dynamic _stateProperties;

  void showByPixel(double x, double y) {
    if (_stateProperties != null) {
      final TooltipRenderingDetails renderingDetails =
          TooltipHelper.getRenderingDetails(_stateProperties.renderingDetails.tooltipBehaviorRenderer);
      renderingDetails.internalShowByPixel(x, y);
    }
  }

  void show(dynamic x, double y, [String? xAxisName, String? yAxisName]) {
    if (_stateProperties != null && _stateProperties.chart is CgCartesianChart) {
      final dynamic chart = _stateProperties.chart;
      final RenderingDetails renderingDetails = _stateProperties.renderingDetails;
      final TooltipBehaviorRenderer tooltipBehaviorRenderer = renderingDetails.tooltipBehaviorRenderer;
      bool? isInsidePointRegion = false;
      ChartAxisRendererDetails? xAxisDetails;
      ChartAxisRendererDetails? yAxisDetails;
      if (xAxisName != null && yAxisName != null) {
        for (final ChartAxisRenderer axisRenderer in _stateProperties.chartAxis.axisRenderersCollection) {
          final ChartAxisRendererDetails axisDetails = AxisHelper.getAxisRendererDetails(axisRenderer);
          if (axisDetails.name == xAxisName) {
            xAxisDetails = axisDetails;
          } else if (axisDetails.name == yAxisName) {
            yAxisDetails = axisDetails;
          }
        }
      } else {
        xAxisDetails = _stateProperties.chartAxis.primaryXAxisDetails;
        yAxisDetails = _stateProperties.chartAxis.primaryYAxisDetails;
      }
      final ChartLocation position = calculatePoint(
          (x is DateTime && (xAxisDetails! is DateTimeCategoryAxisDetails) == false)
              ? x.millisecondsSinceEpoch
              : ((x is DateTime && xAxisDetails! is DateTimeCategoryAxisDetails)
                  ? (xAxisDetails as DateTimeCategoryAxisDetails).labels.indexOf(xAxisDetails.dateFormat.format(x))
                  : ((x is String && xAxisDetails is CategoryAxisDetails) ? xAxisDetails.labels.indexOf(x) : x)),
          y,
          xAxisDetails!,
          yAxisDetails!,
          _stateProperties.requireInvertedAxis,
          null,
          _stateProperties.chartAxis.axisClipRect,);
      for (int i = 0; i < _stateProperties.chartSeries.visibleSeriesRenderers.length; i++) {
        final SeriesRendererDetails seriesRendererDetails =
            SeriesHelper.getSeriesRendererDetails(_stateProperties.chartSeries.visibleSeriesRenderers[i]);
        if (seriesRendererDetails.visible! == true &&
            seriesRendererDetails.series.enableTooltip == true &&
            seriesRendererDetails.regionalData != null) {
          final double padding = (seriesRendererDetails.seriesType == 'bubble' ||
                  seriesRendererDetails.seriesType == 'scatter' ||
                  seriesRendererDetails.seriesType.contains('column') == true ||
                  seriesRendererDetails.seriesType.contains('bar') == true)
              ? 0
              : 15;
          seriesRendererDetails.regionalData!.forEach((dynamic regionRect, dynamic values) {
            final Rect region = regionRect[0];
            final Rect paddedRegion = Rect.fromLTRB(
                region.left - padding, region.top - padding, region.right + padding, region.bottom + padding,);
            if (paddedRegion.contains(Offset(position.x, position.y))) {
              isInsidePointRegion = true;
            }
          });
        }
      }
      if (chart.tooltipBehavior.builder != null && x != null) {
        renderingDetails.tooltipBehaviorRenderer._tooltipRenderingDetails
            .showTemplateTooltip(Offset(position.x, position.y));
      } else if (renderingDetails.tooltipBehaviorRenderer._tooltipRenderingDetails.tooltipTemplate == null) {
        final CgTooltipState? tooltipState = tooltipBehaviorRenderer._tooltipRenderingDetails.chartTooltipState;
        if (isInsidePointRegion ?? false) {
          tooltipBehaviorRenderer._tooltipRenderingDetails.showTooltip(position.x, position.y);
        } else {
          tooltipBehaviorRenderer._tooltipRenderingDetails.show = true;
          tooltipState?.needMarker = false;
          renderingDetails.tooltipBehaviorRenderer._tooltipRenderingDetails
              .showChartAreaTooltip(Offset(position.x, position.y), xAxisDetails, yAxisDetails, chart);
        }
      }
      tooltipBehaviorRenderer._tooltipRenderingDetails.isInteraction = false;
    }
  }

  void showByIndex(int seriesIndex, int pointIndex) {
    if (_stateProperties != null) {
      final TooltipRenderingDetails renderingDetails =
          TooltipHelper.getRenderingDetails(_stateProperties.renderingDetails.tooltipBehaviorRenderer);
      renderingDetails.internalShowByIndex(seriesIndex, pointIndex);
    }
  }

  void hide() {
    if (_stateProperties != null) {
      final TooltipBehaviorRenderer tooltipBehaviorRenderer = _stateProperties.renderingDetails.tooltipBehaviorRenderer;
      tooltipBehaviorRenderer._tooltipRenderingDetails.showLocation = null;
      tooltipBehaviorRenderer._tooltipRenderingDetails.show = false;
      if (builder != null) {
        tooltipBehaviorRenderer._tooltipRenderingDetails.chartTooltipState?.hide(hideDelay: 0);
      } else {
        tooltipBehaviorRenderer._tooltipRenderingDetails.currentTooltipValue =
            tooltipBehaviorRenderer._tooltipRenderingDetails.prevTooltipValue = null;

        tooltipBehaviorRenderer._tooltipRenderingDetails.chartTooltipState?.hide(hideDelay: 0);
      }
    }
  }
}

class TooltipBehaviorRenderer with ChartBehavior {
  TooltipBehaviorRenderer(this._stateProperties) {
    _tooltipRenderingDetails = TooltipRenderingDetails(_stateProperties);
  }

  final dynamic _stateProperties;

  late TooltipRenderingDetails _tooltipRenderingDetails;

  void _hideMouseTooltip() => _tooltipRenderingDetails.hide();

  @override
  void onPaint(Canvas canvas) {}

  @override
  void onDoubleTap(double xPos, double yPos) => _tooltipRenderingDetails.tooltipBehavior.showByPixel(xPos, yPos);

  @override
  void onLongPress(double xPos, double yPos) => _tooltipRenderingDetails.tooltipBehavior.showByPixel(xPos, yPos);

  @override
  void onTouchDown(double xPos, double yPos) => _tooltipRenderingDetails.tooltipBehavior.showByPixel(xPos, yPos);

  @override
  void onTouchMove(double xPos, double yPos) {}

  @override
  void onTouchUp(double xPos, double yPos) => _tooltipRenderingDetails.tooltipBehavior.showByPixel(xPos, yPos);

  @override
  void onEnter(double xPos, double yPos) => _tooltipRenderingDetails.tooltipBehavior.showByPixel(xPos, yPos);

  @override
  void onExit(double xPos, double yPos) {
    if (_tooltipRenderingDetails.renderBox != null && _tooltipRenderingDetails.tooltipBehavior.builder != null) {
      _hideMouseTooltip();
    } else if (_tooltipRenderingDetails.tooltipTemplate != null) {
      _tooltipRenderingDetails.timer?.cancel();
      _tooltipRenderingDetails.timer =
          Timer(Duration(milliseconds: _tooltipRenderingDetails.tooltipBehavior.duration.toInt()), () {});
    }
  }
}

class TooltipValue {
  TooltipValue(this.seriesIndex, this.pointIndex, [this.outlierIndex]);

  final int? seriesIndex;

  final int pointIndex;

  final int? outlierIndex;

  Offset? pointerPosition;

  @override
  bool operator ==(Object other) {
    if (other is TooltipValue) {
      return seriesIndex == other.seriesIndex &&
          pointIndex == other.pointIndex &&
          outlierIndex == other.outlierIndex &&
          (pointerPosition == null || pointerPosition == other.pointerPosition);
    } else {
      return false;
    }
  }

  @override
  int get hashCode => Object.hash(seriesIndex, pointIndex, outlierIndex, pointerPosition);
}

class TooltipHelper {
  const TooltipHelper._();

  static TooltipRenderingDetails getRenderingDetails(TooltipBehaviorRenderer renderer) {
    return renderer._tooltipRenderingDetails;
  }

  static void setStateProperties(TooltipBehavior tooltipBehavior, dynamic stateProperties) {
    tooltipBehavior._stateProperties = stateProperties;
  }
}
